#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <string.h>
#include <stdbool.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>

#define SERVER_PORT 12300
#define SERVER_ADDR "127.0.0.1 "

enum SocketCmd
{
    CMD_SOCKET_REGSITER = 1, // 注册
    CMD_SOCKET_LOGIN,        // 登录

    CMD_SOCKET_ADDFRIEND, // 添加好友
    CMD_SOCKET_DELFRIEND, // 删除好友
    CMD_SOCKET_CHATGPT,   // 聊天
    CMD_SOCKET_EXIT,      // 退出
};

// 链表 数据结构体
typedef struct
{
    int fd;            // 客户端的文件描述符
    char account[256]; // 客户端 的 账号
    char passwd[256];  // 客户端 的密码

    struct sockaddr_in addr; // 客户端的IP地址和端口号
    char msg[1024];
} NodeData_t;

// 链表数据结点的设计
typedef struct node
{

    NodeData_t data;   // 数据域
    struct node *next; // 指针域

} Node_t;

// 链表的 管理结构体（头节点）
struct list
{
    Node_t *head; // 存储链表的数据首结点的地址
    Node_t *tail; // 存储链表的数据尾结点的地址

    int nodeNumber; // 记录链表数据结点的数量
};

// 以下是 客户端  发送  数据过来的时候 ，数据的类型

enum ReplyState
{
    CMD_SOCKET_REPLY_OK = 100, // 应答成功
    CMD_SOCKETT_REPLY_FAIL     // 应答失败
};

// 网络 数据结构体
typedef struct
{
    int cmd;                  // 命令控制位  enum SocketCmd
    int replyState;           // 应答标志位   enum ReplyState
    char replyErrorCode[256]; // 如果应答失败，则把错误的原因放在这里
    char account[256];        // 账号
    char passwd[256];         // 密码

    struct sockaddr_in addr; // 存储IP地址和端口号

    union
    {
        char msg[1024]; // 聊天的数据

    } cmdData;

} SockData_t;

int socketFd;
int g_udpSocketFd;
struct list *list;

/*-----------------初始化链表-------------------*/

int initList()
{
    // 申请 链表的管理结构体内存空间
    list = malloc(sizeof(struct list));
    if (list == NULL)
    {
        perror("malloc list error");
        return -1;
    }

    list->head = list->tail = NULL;
    list->nodeNumber = 0;

    return 0;
}

/*-----------------更新链表节点-------------------*/

int insertNodeToList(NodeData_t inputData)
{
    // 1、先判断 头节点不为NULL
    if (list == NULL)
    {
        return -1;
    }
    // 2、申请新数据结点内存空间并且初始化
    Node_t *newNode = malloc(sizeof(Node_t));
    if (newNode == NULL)
    {
        perror("malloc newNode error");
        return -1;
    }
    newNode->data = inputData;
    newNode->next = NULL;

    // 3、判断当前链表的数据结点是为 0
    if (list->nodeNumber == 0)
    {
        // 头节点的head 和 tail  都指向 这一个数据结点
        list->head = newNode;
        list->tail = newNode;
    }
    else
    {

        list->tail->next = newNode;

        // 更新尾结点
        list->tail = newNode;
    }

    // 4、结点数量+1
    list->nodeNumber++;
}

/*-----------------初始化Tcp-------------------*/

int initTcp()
{
    // 1、建立套接字
    socketFd = socket(AF_INET, SOCK_STREAM, 0);
    if (socketFd == -1)
    {
        perror("socket error");
        return -1;
    }

    // 2、端口号服用
    int opt = 1;
    setsockopt(socketFd, SOL_SOCKET, SO_REUSEADDR, &opt, (socklen_t)sizeof(opt));
    // 3、绑定自己的IP地址和端口号
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY 代表本机所有的IP地址
    serverAddr.sin_port = htons(SERVER_PORT);
    int ret = bind(socketFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
    if (ret == -1)
    {
        perror("bind error");
        return -1;
    }
    // 4、设置监听
    listen(socketFd, 5);
}

/*-----------------Udp接收数据-------------------*/

void recvDataHandle(int num)
{
    // 定义一个结构体变量，存储 客户端使用UDP的方式 发送过来的数据
    SockData_t data;
    memset(&data, 0, sizeof(data));

    struct sockaddr_in clientAddr;
    int len = sizeof(clientAddr);

    int ret = recvfrom(g_udpSocketFd, &data, sizeof(data), 0, (struct sockaddr *)&clientAddr, &len);

    printf("来自客户端 IP:%s Port:%d 发送的数据:%s \n",
           inet_ntoa(clientAddr.sin_addr),
           ntohs(clientAddr.sin_port),
           data.cmdData.msg);
    printf("需要转发给 客户端[%s] IP:%s 端口号:%d\n",
           data.account,
           inet_ntoa(data.addr.sin_addr),
           ntohs(data.addr.sin_port));
    printf("*********************************************\n");

    // 接下来就是服务器做转发处理
    ret = sendto(g_udpSocketFd, &data, sizeof(data), 0, (struct sockaddr *)&(data.addr), sizeof(struct sockaddr));
}

/*-----------------Udp初始化-------------------*/

int initUdp()
{
    int ret;

    // 1、建立套接字
    g_udpSocketFd = socket(AF_INET, SOCK_DGRAM, 0);
    if (g_udpSocketFd == -1)
    {
        perror("socketFd error");
        return -1;
    }

    // 端口号服用
    int opt = 1;
    setsockopt(g_udpSocketFd, SOL_SOCKET, SO_REUSEADDR, &opt, (socklen_t)sizeof(opt));

    // 2、绑定自己的IP地址和端口号
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(SERVER_PORT);
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    ret = bind(g_udpSocketFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
    if (ret == -1)
    {
        perror("bind error");
        return -1;
    }
    // 3、设置信号的SIGIO的信号响应函数
    // 也就是说，当有客户端发送数据过来的时候，内核通过套接字会自动发射SIGIO信号
    signal(SIGIO, recvDataHandle);

    // 4、设置信号SIGIO的属主进程
    // 也就是说，当内核发射出这个SIGIO信号之后，设置哪个进程去响应它
    fcntl(g_udpSocketFd, F_SETOWN, getpid());

    // 5、默认情况下，客户端有数据发送过来的时候，内核不会自动发射SIGIO信号
    // 也就是说 这个模式是关闭的，那么现在要开启生效它，就是有数据发来的时候，内核自动发射该信号
    // 1)先获取原来套接字文件描述符的属性
    int status = fcntl(g_udpSocketFd, F_GETFL);
    // 2）将获取到的属性变量中 加入 信号触发模式功能
    status |= O_ASYNC;
    // 3)把修改好的属性变量 重新设置回套接字中
    fcntl(g_udpSocketFd, F_SETFL, status);

    return 0;
}

/*-----------------接收注册信号处理-------------------*/

int reply_register(SockData_t data, Node_t *p)
{
    // 应答
    // 1、先遍历链表，查看当前注册的这个账号是否 已经 被注册（已经存在）
    Node_t *p1 = list->head;
    bool registerFlag = false; // 是否被注册  如果为true 说明被注册了
    while (p1)
    {
        if (strcmp(data.account, p1->data.account) == 0)
        {
            // 说明已经被注册了
            registerFlag = true;
            // registerFlag != registerFlag;
            break;
        }
        p1 = p1->next;
    }

    // 2、如果没有被注册，那么 组合 数据包，标志位 replyState  为 CMD_SOCKET_REPLY_OK
    if (registerFlag == false)
    {
        // 1）因为没有被注册过，所以 还要把 当前注册好的账号和 密码 更新到 链表中
        strcpy(p->data.account, data.account);
        strcpy(p->data.passwd, data.passwd);

        // 2）组合数据包 回复 注册成功

        data.replyState = CMD_SOCKET_REPLY_OK;
        strcpy(data.replyErrorCode, "success");

        printf("客户端 账号:%s 密码:%s 注册成功...\n", p->data.account, p->data.passwd);
    }
    else
    { // 3、如果被注册了，那么 组合数据包 标志位 replyState  为  CMD_SOCKETT_REPLY_FAIL
        data.replyState = CMD_SOCKETT_REPLY_FAIL;
        strcpy(data.replyErrorCode, "account exists");

        printf("客户端 账号:%s 密码:%s 注册失败 该账号已经存在...\n", data.account, data.passwd);
    }

    // 4、发送 组合好的数据 给 客户端
    write(p->data.fd, &data, sizeof(data));
}

/*-----------------接收登录信号处理-------------------*/

int reply_login(SockData_t data, Node_t *p)
{
    // 1、遍历链表，查看登录的那个账号是否存在，如果存在则比较密码
    Node_t *p1 = list->head;
    bool flag = false; // 是否找到的标志位
    while (p1)
    {
        if (strcmp(data.account, p1->data.account) == 0)
        {
            // 说明已经找到了
            flag = true;
            break;
        }
        p1 = p1->next;
    }
    // 2、组合网络包 数据
    if (flag == true) // 说明找到 登录的那个账号
    {
        if (strcmp(data.passwd, p1->data.passwd) == 0) // 判断密码是否正确
        {
            // 密码相同
            data.replyState = CMD_SOCKET_REPLY_OK;
            strcpy(data.replyErrorCode, "success");
        }
        else
        { // 密码错了
            data.replyState = CMD_SOCKETT_REPLY_FAIL;
            strcpy(data.replyErrorCode, "login passwd error");
        }
    }
    else
    { // 没有找到该账号
        data.replyState = CMD_SOCKETT_REPLY_FAIL;
        strcpy(data.replyErrorCode, "login account not exits");
    }
    
    printf("客户端 账号:%s 密码:%s 登录成功...\n", p->data.account, p->data.passwd);
    printf("*********************************************\n");

    // 3、给客户端进行应答
    write(p->data.fd, &data, sizeof(data));
}

/*-----------------接收添加好友信号处理-------------------*/

int reply_addFriend(SockData_t data, Node_t *p)
{
    printf("客户端[%s] 添加客户端[%s]为好友  %s\n", p->data.account,data.account, __FUNCTION__);
    printf("添加成功\n");
    printf("*********************************************\n");

    // 1、遍历链表，查看 你要添加的那个好友账号data.account  是否 存在
    Node_t *p1 = list->head;
    bool flag = false; // 是否找到的标志位

    while (p1)
    {
        if (strcmp(data.account, p1->data.account) == 0)
        {
            // 说明已经找到了
            flag = true;
            break;
        }
        p1 = p1->next;
    }

    // 组合数据包
    memset(&data, 0, sizeof(data));
    // 2、如果存在，把该用户的账号 、IP地址和端口号 封装成数据包  应答OK
    if (flag == true)
    {
        data.replyState = CMD_SOCKET_REPLY_OK;
        strcpy(data.replyErrorCode, "success");

        strcpy(data.account, p1->data.account); // 用户的账号
        data.addr = p1->data.addr;              // IP地址和端口号
    }
    else
    {
        // 3、如果不存在，则应答  Failed
        data.replyState = CMD_SOCKETT_REPLY_FAIL;
        strcpy(data.replyErrorCode, "add friend fail,account not exits");
    }

    // 3、给客户端进行应答
    write(p->data.fd, &data, sizeof(data));
}

/*-----------------接收删除好友信号处理-------------------*/

int reply_delFriend(SockData_t data, Node_t *p)
{
    printf("客户端[%s] 删除客户端[%s]为好友  %s\n", p->data.account,data.account, __FUNCTION__);
    printf("删除成功\n");
    printf("*********************************************\n");

    // 1、遍历链表，查看 你要添加的那个好友账号data.account  是否 存在
    Node_t *p1 = list->head;
    bool flag = false; // 是否找到的标志位

    while (p1)
    {
        if (strcmp(data.account, p1->data.account) == 0)
        {
            // 说明已经找到了
            flag = true;
            break;
        }
        p1 = p1->next;
    }

    // 组合数据包
    memset(&data, 0, sizeof(data));
    // 2、如果存在，把该用户的账号 、IP地址和端口号 封装成数据包  应答OK
    if (flag == true)
    {
        data.replyState = CMD_SOCKET_REPLY_OK;
        strcpy(data.replyErrorCode, "success");

        strcpy(data.account, p1->data.account); // 用户的账号
        data.addr = p1->data.addr;              // IP地址和端口号
    }
    else
    {
        // 3、如果不存在，则应答  Failed
        data.replyState = CMD_SOCKETT_REPLY_FAIL;
        strcpy(data.replyErrorCode, "add friend fail,account not exits");
    }

    // 3、给客户端进行应答
    write(p->data.fd, &data, sizeof(data));
}

void analysisRecvData(SockData_t data, Node_t *p)
{
    switch (data.cmd)
    {
    case CMD_SOCKET_REGSITER: // 注册
        reply_register(data, p);
        break;
    case CMD_SOCKET_LOGIN: // 登录
        reply_login(data, p);
        break;
    case CMD_SOCKET_ADDFRIEND: // 添加好友
        reply_addFriend(data, p);
        break;
    case CMD_SOCKET_DELFRIEND: // 删除好友
        reply_delFriend(data, p);
        break;
    default:
        break;
    }
}
int main()
{
    printf("服务器端上线.....\n");

    fd_set set, allset;
    int maxFd;

    // TCP初始化  ---多路复用
    initTcp();
    // UDP初始化  ---信号驱动模型
    initUdp();

    initList();

    // 初始化文件描述符集合变量
    FD_ZERO(&set);
    FD_ZERO(&allset);
    // 将监听套接字加入到集合变量中
    FD_SET(socketFd, &allset);

    // 最大文件描述符
    maxFd = socketFd;

    while (1)
    {
        set = allset;
        // 同时监听多个套接字  nready 表示 发生变化的文件描述符的个数
        int nready = select(maxFd + 1, &set, NULL, NULL, NULL);

        // 走到这里，说明监听的多个套接字 的状态有发生改变
        if (FD_ISSET(socketFd, &set))
        {
            // 接收多个客户端的连接
            struct sockaddr_in clientAddr;
            int len = sizeof(clientAddr);

            int newClientFd = accept(socketFd, (struct sockaddr *)&clientAddr, &len);
            int cnt ;
            printf("\n第[%d]位客户端连接上来了 IP:%s Port:%d\n", cnt++,inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

            // 要把新的客户端文件描述符 加入到 文件集合变量中
            FD_SET(newClientFd, &allset);

            // 最大文件描述符进行更新
            if (newClientFd > maxFd)
                maxFd = newClientFd;

            // 把客户端文件描述符 存储 到 链表中
            NodeData_t data;
            data.fd = newClientFd;

            data.addr = clientAddr;

            insertNodeToList(data);

            if (--nready == 0)
                continue;
        }

        // 遍历链表，查看是否有客户端发送数据过来
        Node_t *p = list->head;
        while (p) // p == NULL
        {
            if (FD_ISSET(p->data.fd, &set))
            {
                SockData_t data; // 客户端以结构体 SockData_t 的形式发送数据，那么需要定义这样的结构体去 接收数据
                memset(&data, 0, sizeof(data));

                // 读取数据
                read(p->data.fd, &data, sizeof(data));

               // printf("客户端 IP:[%s] Port:%d 发送数据过来，准备解析....\n",
                      // inet_ntoa(p->data.addr.sin_addr),
                      // ntohs(p->data.addr.sin_port));

                // 解析数据
                analysisRecvData(data, p);

                if (--nready == 0)
                    break;
            }
            p = p->next;
        }
    }

    return 0;
}